Skip to content

Backport: eager connect watchdog for InPlay/iPhone-16 wedges - #141

Open
ps2 wants to merge 2 commits into
devfrom
backport/eager-connect-dev
Open

Backport: eager connect watchdog for InPlay/iPhone-16 wedges#141
ps2 wants to merge 2 commits into
devfrom
backport/eager-connect-dev

Conversation

@ps2

@ps2 ps2 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Backport of #133 to the dev line, squashed to a single commit.

What it brings

Mitigation for BLE connection wedges on InPlay-firmware DASH pods paired with an iPhone 16 (any variant) or 17e: a connect that stalls with no callback is torn down and retried rather than left to time out, plus a persistent notice in pod settings explaining the slower connections and a detail screen behind it.

It also carries the later fixes from that branch:

  • O5 fresh-advert connect — the fast-connect path was nested inside a DASH-only branch, so an Omnipod 5 could never take it. Foreground connects measured 5.9s against 0.4s for DASH.
  • Foreground-state seed — a pump manager constructed after the app has already become active never saw didBecomeActive, so isAppForeground stayed false and the link was idle-disconnected ~4s after each command while the user was on screen.
  • Test-beeps button no longer gates on hasConnection.

Conflicts

Rebased onto dev at 2c603f7. Two conflicts, both with #142 (the Pod Keep Alive backport), which introduced newer accessors that this branch predated.

Neither resolution is a judgment call — each reproduces exactly what next-dev already carries, that being the lineage where both changes coexist.

1. BluetoothManager.shouldHoldConnection — kept dev's podKeepAliveKeepsConnectedInBackground accessor and layered the eager-connect early return above it:

if isAppForeground { return true }
if let peripheral = keepAlivePeripheral, shouldUseEagerConnect(for: peripheral) { return true }
return podKeepAliveKeepsConnectedInBackground

Byte-identical to next-dev.

2. OmniPumpManager pairing auto-enable — a genuine split; took each side's newer half, which is again what next-dev has:

if self.usingInPlayPod == true && UIDevice.hasPossibleInPlayBLEIssues {   // ours (consolidated helper)
    if self.state.podKeepAlive == .disabled {                            // dev's, from #142

Storage.shared.podKeepAlive.value no longer appears anywhere in the tree. The backport removes dev's local OmniPumpManager.iPhoneWithPossibleInPlayIssues and ships OmnipodKit/Common/UIDevice.swift with the consolidated helper, so there is no leftover duplicate — zero references remain to the old name.

Verification

** BUILD SUCCEEDED **, exit 0 — LoopWorkspace scheme, iPhone 17 simulator, LoopWorkspace dev with this branch checked out for OmnipodKit.

One unrelated fix was required to get there: dev still has the usesRileyLink break that #146 fixed on main, so OmnipodKit/dev cannot currently build for any simulator destination, with or without this PR. Raised separately as #148. With that applied, this branch builds clean — it was the only failure.

Not exercised on hardware. Testing on an InPlay pod with an iPhone 16 or 17e is the remaining gap, which is why this stays a draft.

Note on the earlier "does not build against LoopWorkspace/dev" section

That section is obsolete and has been removed. It described OmnipodKit/dev requiring PumpHeartbeatRequest, a LoopKit type the pinned LoopKit did not define. That has since landed via LoopKit #599 — LoopWorkspace/dev now pins LoopKit a5beee96, which defines it. The dependency is satisfied and this PR is no longer blocked on a LoopKit bump.

Backport of #133 from next-dev, squashed. Mitigates BLE connection
wedges on InPlay-firmware DASH pods paired with an iPhone 16 (any
variant) or 17e: a connect that stalls with no callback is torn down and
retried rather than left to time out, and the pod settings view gains a
persistent notice explaining the slower connections.

Also carries the later fixes from that branch: the O5 fresh-advert
connect, which cut foreground connects on Omnipod 5 from about six
seconds to under one; the foreground-state seed, so a pump manager built
after the app has already become active does not idle-disconnect between
commands; and the test-beeps button no longer gating on hasConnection.

Two conflicts against this line, both because dev predates the Pod Keep
Alive rework (#125). Resolved by keeping dev's accessor and taking the
eager-connect logic:

- shouldHoldConnection keeps podType.isDash &&
  Storage.shared.podKeepAlive.value.keepsPodConnectedInBackground rather
  than podKeepAliveKeepsConnectedInBackground
- the pairing auto-enable keeps Storage.shared.podKeepAlive.value while
  adopting the consolidated UIDevice.hasPossibleInPlayBLEIssues

Does not build against LoopWorkspace/dev as it stands, for reasons that
predate this change -- see the pull request.
@ps2

ps2 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current dev (2c603f7) and reconciled the conflicts with #142 (Pod Keep Alive backport). Still a single squashed commit.

Conflicts, and how they were resolved

Both were the same shape: #142 introduced newer accessors on dev, while this backport was written against the pre-#142 forms. In each case the resolution reproduces exactly what next-dev already carries — the lineage where both changes coexist — rather than being a judgment call.

1. BluetoothManager.shouldHoldConnection

dev replaced the direct Storage.shared lookup with the podKeepAliveKeepsConnectedInBackground accessor. Kept dev's accessor and layered the eager-connect early return on top:

if isAppForeground { return true }
if let peripheral = keepAlivePeripheral, shouldUseEagerConnect(for: peripheral) { return true }
return podKeepAliveKeepsConnectedInBackground

Byte-identical to next-dev.

2. OmniPumpManager pairing auto-enable

A genuine split — took each side's newer half, which is again what next-dev has:

if self.usingInPlayPod == true && UIDevice.hasPossibleInPlayBLEIssues {   // ours (consolidated helper)
    if self.state.podKeepAlive == .disabled {                            // dev's, from #142

Storage.shared.podKeepAlive.value is gone from the tree entirely after this. The commit removes dev's local iPhoneWithPossibleInPlayIssues and ships OmnipodKit/Common/UIDevice.swift with the consolidated helper, so there's no leftover duplicate — zero references remain to the old name.

The original blocker is gone

This PR was opened noting it couldn't build until dev's LoopKit gained PumpHeartbeatRequest. That landed via LoopKit #599, and LoopWorkspace dev now pins a5beee96, which has it. No longer blocked.

Build

** BUILD SUCCEEDED **, exit 0 — LoopWorkspace scheme, iPhone 17 simulator, LoopWorkspace dev with this branch checked out for OmnipodKit.

That required one unrelated fix: dev has the same usesRileyLink break that #146 fixed on main, so OmnipodKit/dev currently cannot build for any simulator destination. Raised separately as #148. With that applied, this branch builds clean — it was the only failure.

Still a draft: hardware testing on an InPlay pod + iPhone 16/17e is the remaining gap.

@ps2
ps2 marked this pull request as ready for review September 4, 2026 22:20
itsmojo
itsmojo previously approved these changes Sep 5, 2026

@itsmojo itsmojo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge looks good and basic functionality checks out on a non-iPhone 16/17e. Unfortunately I am unable to perform any real testing of the new eager connect functionality without having an iPhone that exhibits the problematic behavior with an InPlay BLE DASH pod.

setBLEHeartbeatRequest branched on podType.mayUseRileyLink, which is true
for DASH as well as Eros because DASH can use a RileyLink under the Pod
Keep Alive option. A DASH pod on direct BLE with Pod Keep Alive off
therefore took the RileyLink branch and only set timerTickEnabled on a
RileyLink provider that has no devices. provideHeartbeat was never
assigned and BlePodComms.setHeartbeatRequest was never called, so the
delayed-connect probe that produces periodic background wakes was never
scheduled. issueHeartbeatIfNeeded() also gates on provideHeartbeat, so
even a probe that did fire would not have notified Loop.

Loop was requesting the heartbeat correctly -- DeviceDataManager sets
pumpManagerMustProvideBLEHeartbeat when the CGM cannot provide one -- and
the request was logged and then dropped. With no wake source the app
stayed suspended in the background until something else woke it. Seen as
Loop Failure alerts after gaps of 20 to 264 minutes that end the instant
the app is foregrounded; a report from an affected DASH pod shows
provideHeartbeat: false moments after a setBLEHeartbeatRequest log line.

Only reachable when the CGM cannot supply the heartbeat itself, so it
needs a remote or networked CGM such as Nightscout. A BLE Dexcom provides
its own heartbeat and masks it.

Branch on whether a RileyLink is actually in use instead: Eros always,
DASH only under the RileyLink Pod Keep Alive option. The other
mayUseRileyLink call sites want the "may" semantics and are unchanged.
@ps2

ps2 commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Added a second commit (0f3295f) for a separate background-wake bug found while testing this branch on a DASH pod with Nightscout Remote CGM.

setBLEHeartbeatRequest branched on podType.mayUseRileyLink, which is true for DASH as well as Eros because DASH can use a RileyLink under Pod Keep Alive. A DASH pod on direct BLE with Pod Keep Alive off took the RileyLink branch and set timerTickEnabled on a provider with no devices, so provideHeartbeat was never assigned and BlePodComms.setHeartbeatRequest was never called — the delayed-connect probe that produces periodic background wakes was never scheduled. issueHeartbeatIfNeeded() also gates on provideHeartbeat, so even a probe that fired would not have notified Loop.

Loop was asking correctly: DeviceDataManager.pumpManagerMustProvideBLEHeartbeat is true when the CGM cannot provide a heartbeat, and the request reaches us — the [heartbeat] setBLEHeartbeatRequest(last=... interval=300s) line is right there in the log — and then gets dropped. With no wake source the app stays suspended until something else wakes it. In one report: Omni activity stops at 06:33:12 on background — disconnecting, resuming heartbeat probe, nothing at all for 20m32s, Loop has not completed successfully in 20 minutes at 06:53:37, then foreground at 06:53:44 and connected in 2.9s. Same shape at 29, 39 and 264 minutes on previous days. The report shows provideHeartbeat: false under a minute after a setBLEHeartbeatRequest log line, which is the direct fingerprint.

It needs a CGM that cannot supply its own heartbeat, so remote/networked CGMs like Nightscout — a BLE Dexcom masks it entirely. That is presumably why it has gone unreported.

The fix branches on whether a RileyLink is actually in use (Eros always; DASH only under the RileyLink Pod Keep Alive option) rather than whether one is possible. I checked the other six mayUseRileyLink call sites — RL notification registration, forgetBluetoothManager, checkRileyLinkBattery, and the two in OmniPumpManagerState — and they all genuinely want the "may" semantics, so they are untouched.

This is orthogonal to the watchdog, so happy to split it into its own PR against dev if you would rather keep this branch focused — say the word.

Separately, one thing in this branch worth a look. The new [foreground] connected Xs after foregrounding metric reads high: a log line at 06:33:05 says 684.2s after foregrounding when the foreground transition was at 06:33:04 and the connect took about a second — 684.2s back-dates to an earlier foregrounding at 06:21:41. foregroundConnectWaitSince[id] is only set when nil (BluetoothManager.swift:1153) and only cleared in didConnect (:1631), so nothing resets it on background; a foreground that does not connect, then a background, then a later foreground reports the whole span including background time. Since that number is the evidence for whether the watchdog is helping, it is currently flattering to it. Instrumentation only, no dosing impact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants